Apollo Server & Error Handling
TL;DR
ApolloServer で Resolver 内から throw される例外は、最終的に ApolloError にラップされる
ApolloError にはスタックトレースの他にエラーコードも含まれ、クライアントはこれを参照してエラーの分類を知ることができる
組み込みのエラーには AuthenticationError, ForbiddenError, UserInputError がある
エラーレスポンスオブジェクトには、任意のフィールドを追加することもできる
カスタムエラーを投げたい場合は、ApolloError を継承したエラーを新たに定義するか、ApolloError 自体を throw するのが良い
Predefined errors
Apollo Server は apollo-server-core を包含していて、さらにその中の apollo-server-errors にいくつかエラーが定義されている。それらのリファレンスが見当たらなかったので、コードをのぞいてみると、例えば以下のようなエラーが定義されている。
AuthenticationError
ForbiddenError
UserInputError
以下は、Apollo Server が独自に吐くエラーのように見える。
SyntaxError GraphQL のパースに失敗
ValidationError GraphQL の検証に失敗
PersistedQueryNotFoundError PersistedQuery が見つからない
PersistedQueryNotSupportedError PersistedQuery をサポートしていない
https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-errors
ApolloError
Apollo Server にはいくつか組み込みのエラー定義があり、それらは全て ApolloError を継承している。
code:typescript
export class ApolloError extends Error implements GraphQLError {
public extensions: Record<string, any>;
readonly name;
readonly locations;
readonly path;
readonly source;
readonly positions;
readonly nodes;
public originalError;
key: string: any;
constructor(
message: string,
code?: string,
properties?: Record<string, any>,
) {
super(message);
if (properties) {
Object.keys(properties).forEach(key => {
thiskey = propertieskey;
});
}
// if no name provided, use the default. defineProperty ensures that it stays non-enumerable
if (!this.name) {
Object.defineProperty(this, 'name', { value: 'ApolloError' });
}
// extensions are flattened to be included in the root of GraphQLError's, so
// don't add properties to extensions
this.extensions = { code };
}
}
例えば、UserInputError は以下のように定義される。
code:typescript
export class UserInputError extends ApolloError {
constructor(message: string, properties?: Record<string, any>) {
super(message, 'BAD_USER_INPUT', properties);
Object.defineProperty(this, 'name', { value: 'UserInputError' });
}
}
通常の Node.js の例外は、toApolloError で ApolloError に変換される。
code:typescript
export function toApolloError(
error: Error & { extensions?: Record<string, any> },
code: string = 'INTERNAL_SERVER_ERROR',
): Error & { extensions: Record<string, any> } {
let err = error;
if (err.extensions) {
err.extensions.code = code;
} else {
err.extensions = { code };
}
return err as Error & { extensions: Record<string, any> };
したがって、Apollo Server は、Resolver 内で例外が発行された際に、最終的にそれを ApolloError として処理する。そして、ApolloError にはエラーコードが含まれており、クライアントはこれを参照してエラーの分類を確認できる。toApolloError で変換されたエラーは、INTERNAL_SERVER_ERROR というエラーコードになる。
https://www.apollographql.com/docs/apollo-server/images/features/error-code.png
ApolloError に情報を追加する
任意のフィールドをエラーオブジェクトに追加できる。
code:typescript
const {
ApolloServer,
UserInputError,
gql,
} = require('apollo-server');
const typeDefs = gql`
type Mutation {
userInputError(input: String): String
}
`;
const resolvers = {
Mutation: {
userInputError: (parent, args, context, info) => {
if (args.input !== 'expected') {
throw new UserInputError('Form Arguments invalid', {
invalidArgs: Object.keys(args),
});
}
},
},
};
https://www.apollographql.com/docs/apollo-server/images/features/error-user-input.png
https://www.apollographql.com/docs/apollo-server/features/errors.html